/* ply-image.c - png file loader
 *
 * Copyright (C) 2006, 2007 Red Hat, Inc.
 * Copyright (C) 2003 University of Southern California
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * Some implementation taken from the cairo library.
 *
 * Shamelessly taken and unmercifully hacked for temporary use
 * in Chrome OS.
 *
 * Written by: Charlie Brej <cbrej@cs.man.ac.uk>
 *             Kristian Høgsberg <krh@redhat.com>
 *             Ray Strode <rstrode@redhat.com>
 *             Carl D. Worth <cworth@cworth.org>
 */

#include <assert.h>
#include <errno.h>
#include <getopt.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

// <png.h> depends on <math.h> but doesn't include it.  Boo!
//
// Also, <png.h> includes <setjmp.h> and is designed to break if we
// re-include it.  Boo!
#include <math.h>
#include <png.h>

#include "ply-frame-buffer.h"
#include "ply-gamma.h"
#include "ply-monitor.h"
#include "ply-utils.h"

#define MS_PER_SEC 1000LL
#define NS_PER_SEC (1000LL * 1000LL * 1000LL)
#define NS_PER_MS  (NS_PER_SEC / MS_PER_SEC)

/* Default framerate for animations. */
#define DEFAULT_FRAMES_PER_SEC 20

typedef union {
  uint32_t *as_pixels;
  png_byte *as_png_bytes;
  char *address;
} ply_image_layout_t;

typedef struct {
  char  *filename;
  FILE  *fp;

  ply_image_layout_t layout;
  size_t size;

  png_uint_32 width;
  png_uint_32 height;
} ply_image_t;


/* Returns the current CLOCK_MONOTONIC time in milliseconds. */
static int64_t get_monotonic_time_ms() {
  struct timespec spec;
  assert(clock_gettime(CLOCK_MONOTONIC, &spec) == 0);
  return MS_PER_SEC * spec.tv_sec + spec.tv_nsec / NS_PER_MS;
}

static bool ply_image_open_file(ply_image_t *image) {
  assert(image != NULL);

  image->fp = fopen(image->filename, "r");

  return image->fp != NULL;
}

static void ply_image_close_file(ply_image_t *image) {
  assert(image != NULL);

  if (image->fp == NULL)
    return;
  fclose(image->fp);
  image->fp = NULL;
}

static ply_image_t *ply_image_new(const char *filename) {
  ply_image_t *image;

  assert(filename != NULL);

  image = calloc(1, sizeof(*image));

  image->filename = strdup(filename);
  image->fp = NULL;
  image->layout.address = NULL;
  image->size = -1;
  image->width = -1;
  image->height = -1;

  return image;
}

static void ply_image_free(ply_image_t *image) {
  if (image == NULL)
    return;

  assert(image->filename != NULL);

  if (image->layout.address != NULL) {
    free(image->layout.address);
    image->layout.address = NULL;
  }

  free(image->filename);
  free(image);
}

/*
 * The following function courtesy of Intel Moblin's
 * plymouth-lite port.
 */
static void transform_to_rgb32(png_struct   *png,
                               png_row_info *row_info,
                               png_byte     *data) {
  unsigned int i;

  for (i = 0; i < row_info->rowbytes; i += 4) {
    uint8_t  red, green, blue, alpha;
    uint32_t pixel_value;

    red = data[i + 0];
    green = data[i + 1];
    blue = data[i + 2];
    alpha = data[i + 3];
    pixel_value = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0);
    memcpy(data + i, &pixel_value, sizeof(pixel_value));
  }
}

static bool ply_image_load(ply_image_t *image) {
  png_struct *png;
  png_info *info;
  png_uint_32 width, height, bytes_per_row, row;
  int bits_per_pixel, color_type, interlace_method;
  png_byte **rows;

  assert(image != NULL);

  if (!ply_image_open_file(image))
    return false;

  png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  assert(png != NULL);

  info = png_create_info_struct(png);
  assert(info != NULL);

  png_init_io(png, image->fp);

  if (setjmp(png_jmpbuf(png)) != 0) {
    ply_image_close_file(image);
    return false;
  }

  png_read_info(png, info);
  png_get_IHDR(png, info,
               &width, &height, &bits_per_pixel,
               &color_type, &interlace_method, NULL, NULL);
  bytes_per_row = 4 * width;

  if (color_type == PNG_COLOR_TYPE_PALETTE)
    png_set_palette_to_rgb(png);

  if ((color_type == PNG_COLOR_TYPE_GRAY) && (bits_per_pixel < 8))
    png_set_gray_1_2_4_to_8(png);

  if (png_get_valid(png, info, PNG_INFO_tRNS))
    png_set_tRNS_to_alpha(png);

  if (bits_per_pixel == 16)
    png_set_strip_16(png);

  if (bits_per_pixel < 8)
    png_set_packing(png);

  if ((color_type == PNG_COLOR_TYPE_GRAY) ||
      (color_type == PNG_COLOR_TYPE_GRAY_ALPHA))
    png_set_gray_to_rgb(png);

  if (interlace_method != PNG_INTERLACE_NONE)
    png_set_interlace_handling(png);

  png_set_filler(png, 0xff, PNG_FILLER_AFTER);

  png_set_read_user_transform_fn(png, transform_to_rgb32);

  png_read_update_info(png, info);

  rows = malloc(height * sizeof(*rows));
  image->layout.address = malloc(height * bytes_per_row);

  for (row = 0; row < height; row++)
    rows[row] = &image->layout.as_png_bytes[row * bytes_per_row];

  png_read_image(png, rows);

  free(rows);
  png_read_end(png, info);
  ply_image_close_file(image);
  png_destroy_read_struct(&png, &info, NULL);

  image->width = width;
  image->height = height;

  return true;
}

static const uint32_t *ply_image_get_data(const ply_image_t *image) {
  assert(image != NULL);

  return image->layout.as_pixels;
}

static long ply_image_get_width(const ply_image_t *image) {
  assert(image != NULL);

  return image->width;
}

static long ply_image_get_height(const ply_image_t *image) {
  assert(image != NULL);

  return image->height;
}

static ply_image_t *ply_image_from_file(const char *path) {
  ply_image_t *image = ply_image_new(path);

  if (!ply_image_load(image)) {
    perror(path);
    exit(errno);
  }
  return image;
}

static int debug = 0;
static bool center_image = true;
static ply_frame_buffer_area_t image_location, image_offset;
static uint32_t clear_color;

static void ply_frame_buffer_show_image(
    ply_frame_buffer_t *buffer, const ply_image_t *image) {
  const uint32_t *data;
  ply_frame_buffer_area_t area, buffer_area;
  bool fill_succeeded;

  area.width = ply_image_get_width(image);
  area.height = ply_image_get_height(image);
  data = ply_image_get_data(image);

  if (center_image) {
    ply_frame_buffer_get_size(buffer, &buffer_area);
    area.x = (long) (buffer_area.visible_width - area.width) / 2;
    area.y = (long) (buffer_area.visible_height - area.height) / 2;
  } else {
    area.x = image_location.x;
    area.y = image_location.y;
  }

  area.x += image_offset.x;
  area.y += image_offset.y;
  if (debug) {
    fprintf(stderr, "displaying %lux%lu image at (%ld, %ld)\n",
                    area.width, area.height, area.x, area.y);
  }
  fill_succeeded = ply_frame_buffer_fill(buffer, &area, data);

  if (!fill_succeeded)
    fprintf(stderr, "image was not displayed\n");
}


static int usage(void) {
  fprintf(stderr,
          "usage: ply-image [OPTION]... [FILE]...\n"
          "Copy one or more images to the framebuffer.\n"
          "\n"
          "Options:\n"
          "  --clear=0xRRGGBB         hexadecimal framebuffer clear color\n"
          "  --debug                  print debugging info to stderr\n"
          "  --frame-interval=MSEC    time interval between images\n"
          "  --gamma=FILE             file containing gamma ramps\n"
          "  --help                   display this message\n"
          "  --location=X,Y           image location relative to top left\n"
          "                           corner of framebuffer\n"
          "  --set-monitors           turn off all non-main monitors\n"
          "  --offset=[+|-]X,[+|-]Y   image location as offset from center\n"
          "                           of framebuffer\n"
          "  --print-resolution       print space-separated framebuffer\n"
          "                           width and height to stdout\n");
  exit(EXIT_FAILURE);
}


static bool parse_color(const char *arg) {
  char *endptr;

  clear_color = strtol(arg, &endptr, 16);
  if (endptr == arg || *endptr != '\0') {
    return false;
  }

  return true;
}

static bool parse_xy(const char *arg, ply_frame_buffer_area_t *area) {
  char *endptr;
  long x, y;

  x = strtol(arg, &endptr, 10);
  if (endptr == arg || *endptr != ',') {
    return false;
  }

  arg = endptr + 1;
  y = strtol(arg, &endptr, 10);
  if (endptr == arg || *endptr != '\0') {
    return false;
  }

  area->x = x;
  area->y = y;

  return true;
}

static bool parse_location(const char *arg) {
  bool result = parse_xy(arg, &image_location);
  if (result)
    center_image = false;
  return result;
}

static bool parse_offset(const char *arg) {
  return parse_xy(arg, &image_offset);
}


/* Frame rate as milliseconds per frame */
static int64_t frame_interval_ms = MS_PER_SEC / DEFAULT_FRAMES_PER_SEC;

static bool set_frame_interval(const char *arg) {
  char *endptr;

  long interval = strtol(arg, &endptr, 10);
  if (endptr == arg || *endptr != '\0') {
    return false;
  }

  frame_interval_ms = interval;
  return true;
}


/*
 * Animate the series of images named on the command line.  An attempt is made
 * to let frame_interval_ms elapse between frames being displayed onscreen,
 * although longer delays can occur.
 */
static void process_images(
    ply_frame_buffer_t *buffer, int argc, char *argv[]) {
  int64_t last_show_ms = -1;

  for (; argc > 0; --argc, ++argv) {
    int64_t now_ms = -1;
    ply_image_t* image = NULL;

    if (debug) {
      fprintf(stderr, "loading %s at %" PRId64 "\n", argv[0],
              get_monotonic_time_ms());
    }
    image = ply_image_from_file(argv[0]);

    now_ms = get_monotonic_time_ms();
    if (last_show_ms > 0) {
      int64_t sleep_ms = frame_interval_ms - (now_ms - last_show_ms);
      if (sleep_ms > 0) {
        struct timespec sleep_spec;
        if (debug) {
          fprintf(stderr, "sleeping %" PRId64 " ms at %" PRId64 "\n",
                  sleep_ms, now_ms);
        }
        sleep_spec.tv_sec = sleep_ms / MS_PER_SEC;
        sleep_spec.tv_nsec = (sleep_ms % MS_PER_SEC) * NS_PER_MS;
        nanosleep(&sleep_spec, NULL);
      }
    }

    now_ms = get_monotonic_time_ms();
    if (debug) {
      fprintf(stderr, "showing %s at %" PRId64 "\n", argv[0], now_ms);
    }
    ply_frame_buffer_show_image(buffer, image);
    ply_image_free(image);
    last_show_ms = now_ms;
  }
}


#define FLAG_CLEAR             'c'
#define FLAG_DEBUG             'D'
#define FLAG_FRAME             'f'
#define FLAG_GAMMA             'g'
#define FLAG_HELP              'h'
#define FLAG_LOCATION          'l'
#define FLAG_SET_MONITORS      'm'
#define FLAG_OFFSET            'o'
#define FLAG_PRINT_RESOLUTION  'p'

static struct option command_options[] = {
  { "clear", required_argument, NULL, FLAG_CLEAR },
  { "debug", no_argument, NULL, FLAG_DEBUG },
  { "frame-interval", required_argument, NULL, FLAG_FRAME },
  { "gamma", required_argument, NULL, FLAG_GAMMA },
  { "help", no_argument, NULL, FLAG_HELP },
  { "location", required_argument, NULL, FLAG_LOCATION },
  { "set-monitors", no_argument, NULL, FLAG_SET_MONITORS },
  { "offset", required_argument, NULL, FLAG_OFFSET },
  { "print-resolution", no_argument, NULL, FLAG_PRINT_RESOLUTION },
  { NULL, 0, NULL, 0 },
};


int main(int argc, char **argv) {
  int clear = 0;
  bool location_assigned = false;
  bool offset_assigned = false;
  bool print_resolution = false;
  ply_frame_buffer_t *buffer;

  /*
   * TODO(jrbarnette): The command line parsing here and throughout
   * the code is a bit rudimentary, especially w.r.t. error
   * checking.  This command is only invoked in a handful of places
   * in Chromium OS; outside of those cases, you should be grateful
   * any time the error message isn't
   *   Segmentation fault (core dumped)
   * :-)
   */
  for (;;) {
    int c = getopt_long(argc, argv, "", command_options, NULL);
    if (c == -1) {
      break;
    }

    if (c == FLAG_HELP) {
      usage();
    }

    if (c == FLAG_DEBUG) {
      debug = 1;
      continue;
    }

    if (c == FLAG_CLEAR) {
      clear = 1;
      if (!parse_color(optarg)) {
        usage();
      }
      continue;
    }

    if (c == FLAG_FRAME) {
      if (!set_frame_interval(optarg)) {
        usage();
      }
      continue;
    }

    if (c == FLAG_GAMMA) {
      // Not all platforms necessarily have a gamma ramp file, so we
      // ignore errors from this call.
      (void) ply_gamma_set(optarg);
      continue;
    }

    if (c == FLAG_LOCATION) {
      if (!parse_location(optarg)) {
        usage();
      }
      location_assigned = true;
      continue;
    }

    if (c == FLAG_SET_MONITORS) {
      ply_monitor_set_monitors = true;
      continue;
    }

    if (c == FLAG_OFFSET) {
      if (!parse_offset(optarg)) {
        usage();
      }
      offset_assigned = true;
      continue;
    }

    if (c == FLAG_PRINT_RESOLUTION) {
      print_resolution = true;
      continue;
    }
  }

  if (location_assigned && offset_assigned) {
    fprintf(stderr,
            "--location and --offset should not be assigned at same time.\n");
    usage();
  }

  buffer = ply_frame_buffer_new(NULL);
  if (!ply_frame_buffer_open(buffer)) {
    perror("could not open framebuffer");
    return errno;
  }

  ply_frame_buffer_area_t fb_area;
  ply_frame_buffer_get_size(buffer, &fb_area);

  if (print_resolution) {
    printf("%zu %zu\n", fb_area.visible_width, fb_area.visible_height);
  }

  if (debug) {
    fprintf(stderr, "screen dimensions:  %lux%lu\n",
            fb_area.width, fb_area.height);
  }

  if (clear) {
    ply_frame_buffer_clear(buffer, clear_color);
  }

  process_images(buffer, argc - optind, argv + optind);
  return EXIT_SUCCESS;
}
